var EXPORTED_SYMBOLS = ["BBSConnection", "tabulator", "kb"];

const LOCATION = "chrome://on-the-wave/content/BBSConnection.js";
const LINEDIFF = 110;

const BBS_USER = 0;
const BBS_PASS = 1;
const BBS_MOVING = 2;
const BBS_MAIN_MENU = 3;
const BBS_BOARD = 4;
const BBS_ARTICLE = 5;
const BBS_FIND_URI = 6;
const ENTER = '\x0D';
const LEFT = '\x1B[D';
const RIGHT = '\x1B[C';

const DOWN = '\x1B[B';
const UP = '\x1B[A';
const PAGE_UP = '\x1B[5~';
const PAGE_DOWN = '\x1B[6~';
const END = '\x1B[4~';

var tabulator = Components.classes["@dig.csail.mit.edu/tabulator;1"].getService(Components.interfaces.nsISupports).wrappedJSObject;
var kb = tabulator.kb;

var FOAF = tabulator.ns.foaf; // for foaf:primaryTopic (see PTTParser.js)

//Creates a string represening an empty screen
function emptyScreen(){
  var ret = "";
  for (var i=0;i<23;i++){
    for (var j=0;j<80;j++)
      ret += " ";
    ret += '\n';
  }
  for (var j=0;j<80;j++)
      ret += " ";
  return ret;
}
function BBSConnection(aHostname){
  //this.mHostname = aHostname;
  this.mCurrentPath = undefined;
  this.mLookupQueue = []; //things to look up later
  this.mCallbackDict = {}; //what to do after looking up
  this.mBuffer = ""; //string collected but not processed yet

  this.mBackendJobs = []; //extra things to do after initial 
                          //loading, these are cancelable
  this.mBackendCallbackDict = {}; //what to do after these backend loads
  this.mBackendJob = ""; // key command for the backend

  //this.mScreenString is the (no color) string in the current screen
  //the static getter is defined below in the prototype definition
  //the followings are non-static variables for the getter to work
  this._restStr = ""; //unclosed escape sequence
  this._cursor = 0;  
  this._screenBytes = emptyScreen();
  

  this.mGoingTo = "/";//the path the connection is going to, also indicates
                      //that the connection is busy
  this.mLookedUp = {}; //cache
  this.mLogIn = [];
  this.mLogOut = [];
  var lm = 
    Cc["@mozilla.org/login-manager;1"].
      getService(Ci.nsILoginManager);
  this.mSiteURI = "bbs://" + aHostname;
  //login info
  
  this.mLogin = lm.findLogins({}, this.mSiteURI, this.mSiteURI, null)[0]; 

  //initiate the connection
  var nsSocketTransportService = 
    Cc["@mozilla.org/network/socket-transport-service;1"].
      getService(Ci.nsISocketTransportService);
  var transport = nsSocketTransportService.
    createTransport(null, 0, aHostname, 23, null);
  this.outputStream = transport.openOutputStream(0,0,0);
  this.inputStream = transport.openInputStream(0,0,0);
  this.converterInputStream = 
    Cc["@mozilla.org/intl/converter-input-stream;1"].
      createInstance(Ci.nsIConverterInputStream);
  this.converterOutputStream =
    Cc["@mozilla.org/intl/converter-output-stream;1"].
      createInstance(Ci.nsIConverterOutputStream);
  this.converterInputStream.init(this.inputStream, "big5", 0xFFFF, 
                        Ci.nsIConverterInputStream.REPLACEMENT_CHARACTER);
  this.converterOutputStream.init(this.outputStream, "big5", 0xFFFF, 
                        Ci.nsIConverterInputStream.REPLACEMENT_CHARACTER);
  var scriptableInputStream = 
    Cc["@mozilla.org/scriptableinputstream;1"].
      createInstance(Ci.nsIScriptableInputStream);
  scriptableInputStream.init(this.inputStream);
  var inputStreamPump =
    Cc["@mozilla.org/network/input-stream-pump;1"].
      createInstance(Ci.nsIInputStreamPump);
  inputStreamPump.init(this.inputStream, -1, -1, 0, 0, false);
  
 //data listener, this calls this.process when a packet arrives
  var conn = this;
  this.dataListener = {
    data: {},
    onStartRequest: function(aRequest, aContext){},
    onStopRequest: function(aRequest, aContext, aStatus){
      dump("disconnected");
      conn.outputStream.close();
      conn.inputStream.close();
      conn.converterInputStream.close();
      conn.converterOutputStream.close();
  
      var bbsProtocolHandler = Components.classes["@mozilla.org/network/protocol;1?name=bbs"].getService().wrappedJSObject;
      delete bbsProtocolHandler.mRootConnectionDict[conn.mSiteURI];
    },
    
    onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount){
      try{
        conn.converterInputStream.readString(0xFFFF, this.data);
        conn.mBuffer = conn.mBuffer + this.data.value;
        conn.mLogIn.push(this.data.value);
        conn.updateScreenString(this.data.value);
        conn.process();
      } catch (e) {
        handleException(e, LOCATION, LINEDIFF);
      }
    }
  };
  this.mState = BBS_USER;
  inputStreamPump.asyncRead(this.dataListener, null);
};

BBSConnection.prototype = {
  process: function process(){
    function readArticle(){
      var articleToRead = this.mGoingTo.split('/')[2];
      if (articleToRead.length === 8){ 
        //article identified by a URI
        this.sendData("#" + articleToRead + ENTER + RIGHT);
        this.mCurrentPath = this.mGoingTo;
        parse_handler.symbol = kb.sym(this.mSiteURI + this.mCurrentPath);
        this.mState = BBS_ARTICLE;
      } else { 
        //article located with the number, find the URI before 
        //loading the article
        this.sendData (articleToRead + ENTER + "Q");
        this.mState = BBS_FIND_URI;
      }
              
    }
    function goToBoard(){
      var boardToGo = this.mGoingTo.split('/')[1];
      this.sendData("s" + boardToGo + ENTER);
      this.mState = BBS_MOVING;
      this.mMovingUtil =  "看板《" + boardToGo;
      //when jumping from board to board, PTT is lazy and wouldn't
      // render "》" orz
      this.mCurrentPath = "/" + boardToGo + "/";
      parse_handler.symbol = kb.sym(this.mSiteURI + this.mCurrentPath);
      this.mNextState = BBS_BOARD;
    }
    switch (this.mState){
    case BBS_USER:
      var i = this.mBuffer.indexOf("請輸入代號");
      if (i!=-1){
        this.sendData(this.mLogin.username + ENTER);
        this.mBuffer = this.mBuffer.substr(i+5);
        this.mState = BBS_PASS;
      }
      break;
    case BBS_PASS:
      var i = this.mBuffer.indexOf("請輸入您的密碼:");
      if(i!=-1){
        this.sendData(this.mLogin.password + ENTER);
        this.mBuffer = this.mBuffer.substr(i+8);
        this.mState = BBS_MOVING;
        
        this.mMovingUtil = "主功能表";
        this.mCurrentPath = "/";
        this.mNextState = BBS_MAIN_MENU;    
      }
      break;
    case BBS_MOVING:
      var i = this.mBuffer.indexOf(this.mMovingUtil);
      if(i!=-1){
        this.mBuffer = this.mBuffer.substr(i+this.mMovingUtil.length);
        this.mState = this.mNextState;
        this.process(); //there might not be any more coming data, so 
                        //push the state machine
        break;
      } 
      else {
        var i = this.mBuffer.indexOf("按任意鍵繼續");
        if(i!=-1){
          this.sendData(ENTER);
          this.mBuffer = this.mBuffer.substr(i+6);
          break;
        }
        var i = this.mBuffer.indexOf("您想刪除其他重複登入的連線嗎？");
        if(i!=-1){
          this.sendData("n" + ENTER);
          this.mBuffer = this.mBuffer.substr(i+15);
          break;
        }
        var i = this.mBuffer.indexOf("您要刪除以上錯誤嘗試的記錄嗎? [y/N]");
        if(i!=-1){
          this.sendData("n" + ENTER);
          this.mBuffer = this.mBuffer.substr(i+21);
          break;
        }
        var i = this.mBuffer.indexOf("【郵件選單】");
        if(i!=-1){
          this.sendData(LEFT);
          this.mBuffer = this.mBuffer.substr(i+6);
          break;
        }
        var i = this.mBuffer.indexOf("信箱容量(大小,非件數)");
        if(i!=-1){
          this.sendData(ENTER);
          this.mBuffer = this.mBuffer.substr(i+12);
          break;
        }
        var i = this.mBuffer.indexOf(">>> 互動式動畫播放中");
        if(i!=-1){
          this.sendData("q");
          this.mBuffer = this.mBuffer.substr(i+12);
          break;
        }
      }
      break;
    case BBS_MAIN_MENU:    
    if (!this.mLookupQueue.length) break; //shouldn't be possible
    this.mGoingTo = this.mLookupQueue.shift();
    if(this.mGoingTo == this.mCurrentPath){
      this.fireCallback();
      break;
    }
    
    goToBoard.call(this); //use call so that 'this' is passed 
    break;
    
    case BBS_BOARD:
      if (this.mGoingTo){
        if (this.mGoingTo == this.mCurrentPath){ //goal arrived
          //var postListFootNote_re = new RegExp(
          //        decode("\x1B[30m進板畫面\x1B[m\x1B[\\d?\\d;2H$"));
          var postListEnd_re = new RegExp(";2H$"); //cursor jump
          if (!postListEnd_re.test(this.mBuffer))
            return; //post list not completed for some reason, so wait 
                    //for next package
          //number of the first article in the page
          var number = parse_handler.parse_postlist(this.mScreenString);
          this.fireCallback();
          if (number !== 1){
            this.addBackendJob(PAGE_UP);
            this.process();
          }
        } else {
          readArticle.call(this);
        }
      } else if (this.mLookupQueue.length) { // get a new URI to read
        this.mGoingTo = this.mLookupQueue.shift();
        dump("the new URI: " + this.mGoingTo + '\n');
        if (this.mGoingTo.substr(0, this.mCurrentPath.length) ==
            this.mCurrentPath) {
          readArticle.call(this);
        } else {
          goToBoard.call(this);
        }
      } else { // do a backend job
        if (!this.mBackendJobs.length) return;
        this.mBackendJob = this.mBackendJobs.shift();
        this.mGoingTo = this.mCurrentPath; //@ the only backend we 
                                           //implement now
        this.sendData(this.mBackendJob);
      }
      break;
    
    case BBS_ARTICLE:
      var pageIndicator_re = new RegExp("瀏覽 第 \\d+/\\d+ 頁" + 
                                        " \\(([ \\d]+)%\\)");
      var result = pageIndicator_re.exec(this.mBuffer);
      //matched if this is the 1st, 2nd, or last page
      if (!result){
        //@check for post with >10 pages, and other cases
        var pageIndicator_re2 = new RegExp("\x1B\\[24;11H" + 
                                           "\x1B\\[34;46m\\d\x1B\\[24;2");
                                           //^^ could jump to 24;21 24;20
      
        if (this.mBuffer.search(pageIndicator_re2) == -1)
          return; //don't do anything to make sure the number of 
                  //RIGHTs is correct
      }
      this.sendData(RIGHT); //keep reading until end of article
      parse_handler.feed(this.mBuffer);
      this.mBuffer = "";
      if (result && result[1] == "100"){
        parse_handler.flush();
        this.fireCallback(); // for cases involving BBS_FIND_URI
        // this would cause a fireCallback -> whenDone -> 
        // window.location.assign -> lookUpLater -> process -> fireCallback
        // -> X loop (it ends because there's no more callback)
        this.mState = BBS_BOARD;
        this.mCurrentPath = '/' + this.mCurrentPath.split('/')[1] + '/';
      }
      break;
    case BBS_FIND_URI:
      if (this.mBuffer.indexOf("請按任意鍵繼續") === -1)
        return; //unfinished package
      var screen = this.mScreenString;
      var aid = screen.substr(screen.indexOf("文章代碼(AID): #") + 12,
                              8);
      var articleURI = this.mSiteURI + this.mCurrentPath + aid;
      this.mCurrentPath = this.mGoingTo; 
      parse_handler.symbol = kb.sym(articleURI);
      //so that fireCallBack would fire bbs://xx/yy/12345
      //but save data with bbs://xx/yy/<aid>
  
      //with this, it can be deduced that the bnode has
      //articleURI (@explain in detail)
      kb.add(kb.sym(this.mSiteURI + this.mCurrentPath), 
             FOAF('primaryTopic'), parse_handler.symbol);
      //jump to BBS_ARTICLE
      this.sendData(ENTER + RIGHT);
      this.mState = BBS_ARTICLE;
      break;
    default:
      throw "unknow BBS state:" + this.mState.toString();
    }
  },
      
  fireCallback: function fireCallback(){
    this.mGoingTo = "";
    var callback = this.mCallbackDict[this.mCurrentPath];
    if (callback){
      callback();
      // it is usually the case that fireCallback -> callback ->
      // process -> fireCallback -> ... . So if the callback is not deleted
      // , there's recursion.
      delete this.mCallbackDict[this.mCurrentPath];
    } else {
      callback = this.mBackendCallbackDict[this.mBackendJob];
      if (callback)
        callback();
      // do not delte the backend callback (@for this moment)
    }
  },

  //backend

  //static private variables for updateScreenString
  _escapeRe: new RegExp("^\\[((?:|\\d+|;)+)([\x40-\x5A\x60-\x7C])"),
  _converter: Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter),
  get mScreenString() {
    return this._converter.ConvertToUnicode(this._screenBytes);
  },
  
  updateScreenString: function(aStr){
    function insert(st){
      if (!st) return; //st is empty => early return
      var bytes = this._converter.ConvertFromUnicode(st);
      var left = this._screenBytes.substring(0, this._cursor);
      var right = this._screenBytes.substring(this._cursor + bytes.length);
      this._screenBytes = left + bytes + right;
      this._cursor += bytes.length;
    }
    var str = this._restStr + aStr;
    this._restStr ='';  
    str = str.replace(/\x07/g, '\x1B\x07');//escape BEL
    str = str.replace(/[\b]/g, '\x1B[1D');//escape BS(replace )
    //str = str.replace(/\t/g, '\x1B[8C');//escape HT(replace )
    //str = str.replace(/\r\n/g, '\x1Bn'); //CRLF
    str = str.replace(/\n/g, '\x1B\n');//escape LF
    //str = str.replace(/\v/g, '\x1B[6B');//escape VT(replace )
    str = str.replace(/\f/g, '\x1B[2J\x1B[H');//escape FF(replace )
    str = str.replace(/\r/g, '\x1B[1G');//escape CR(replace )   
    
    var strArray = str.split('\x1B');
    insert.call(this, strArray[0]);

    for(var i = 1; i < strArray.length; i++){
      switch(strArray[i][0]){
      case '[' :
        var result = this._escapeRe.exec(strArray[i]);
        if(result){
          switch(result[2]){
          case 'm':
            break;
          case 'H':
            var pos = result[1].split(';');
            if (!result[1])
              this._cursor = 0;
            else
              this._cursor = 81 * (pos[0]-1) + (pos[1] - 1);
            if (String(this._cursor) == "NaN")
              throw "not implmemented yet: " + strArray[i];
            break;
          case 'K':
            var left = this._screenBytes.substring(0, this._cursor);
            var spLen = 80 - this._cursor % 81;
            for (var j=0;j<spLen;j++) left += " ";
            var right = this._screenBytes.substring(this._cursor+spLen);
            this._screenBytes = left + right;
            //if (strArray[i][2] != '\n')
            //throw "not implmemented yet: " + strArray[i];               
            break;
          case 'J':
            if (result[1] != "2")
              throw "not implemented yet: " + strArray[i];
            this._screenBytes = emptyScreen();
            break;
          case 'D': // \b:
            if (result[1] != "1")
              throw "not implented yet: " + strArray[i];
            this._cursor -= 1;
            break;
          case 'G': // \r
            if (result[1] != "1")
              throw "not implemented yet: " + strArray[i];
            this._cursor = 81 * Math.floor(this._cursor/81);
            break;
          default:
            throw "[" + this.mLogIn.length + "]not implemented yet: " + strArray[i];
          }
          insert.call(this, strArray[i].substr(result[0].length));
        } 
        else {
          if (i==strArray.length-1)
            this._restStr = '\x1B' + strArray[i];
          else
            throw "escaped sequence not closed: " + strArray[i].toSource();
        }
        break;
      case '\x07': // \b
        break;
      /*  
      case 'n': // \r\n
        this._cursor = 81 * Math.floor(this._cursor/81) + 81;
        insert.call(this, strArray[i].substr(1));
        break;
      */
      case '\n': //\n
        this._cursor += 81;
        insert.call(this, strArray[i].substr(1));
        break;
      default:
        if (i==strArray.length-1)
          this._restStr = '\x1B' + strArray[i];
        else
          throw "[" + i + "]escaped sequence not closed: " + strArray[i].toSource();

      } //End switch
    }//End for 
  },
  sendData: function sendData(aStr){
    this.converterOutputStream.writeString(aStr);
    this.mLogOut.push(aStr);
  },
        
  //these are called by the BBS protocol handler
  lookUpLater: function lookUpLater(aPath){
    try{
      dump("lookingUpLater: " + aPath + '\n');
      /* //disable the cache for the moment
         if(this.mLookedUp[aPath]){
         dump("going to a visited URI, probably triggered by Firebug, hence ignored for this moment\n");
         return;
         }
         this.mLookedUp[aPath] = true;
      */
      
      this.mLookupQueue.push(aPath);
      if (!this.mGoingTo)
        this.process();
    } 
    catch (e) {
      handleException(e, LOCATION, LINEDIFF);
    }
  },

  setCallback: function lookUpLater(aPath, aCallback){
    this.mCallbackDict[aPath] = aCallback;
  },

  // backend job handling
 
  addBackendJob: function addBackendJob(aKey){
    this.mBackendJobs.push(aKey);
  },

  setBackendJobCallback: function setBackendJobCallback(aKey, aCallback){
    //var pathParts = this.mCurrentPath.split('/');
    //pathParts.pop();
    //var boardPath = pathParts.join('/') + '/';
    //this.setCallback(boardPath, 
    //                 aCallback)
    this.mBackendCallbackDict[aKey] = aCallback;

    //    try{
    //this.mBackendJobs.push(aJob);
    //this.mBackendCallbackDict[aJob] = aCallback;
    //if (!this.mGoingTo && !this.mLookupQueue.length)
    //this.process();
        //} catch (e) {
        //handleException(e, LOCATION, LINEDIFF);
        //}
  }
};

BBSConnection.prototype._converter.charset = 'big5';
